package com.yupi.yupicturebackend.manager.websocket;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.yupi.yupicturebackend.manager.websocket.disruptor.PictureEditEventProducer;
import com.yupi.yupicturebackend.manager.websocket.model.PictureEditActionEnum;
import com.yupi.yupicturebackend.manager.websocket.model.PictureEditMessageTypeEnum;
import com.yupi.yupicturebackend.manager.websocket.model.PictureEditRequestMessage;
import com.yupi.yupicturebackend.manager.websocket.model.PictureEditResponseMessage;
import com.yupi.yupicturebackend.manager.websocket.ws.MessageHandler;
import com.yupi.yupicturebackend.model.entity.User;
import com.yupi.yupicturebackend.service.UserService;
import groovyjarjarantlr4.v4.runtime.misc.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 @author 边拾柱
 @date 2025/3/18 16:26
 */
@Component
public class PictureEditHandler extends TextWebSocketHandler {
    @Resource
    private UserService userService;
    @Resource
    private PictureEditEventProducer pictureEditEventProducer;
    // 每张图片的编辑状态，key: pictureId, value: 当前正在编辑的用户 ID
    private final Map<Long, Long> pictureEditingUsers = new ConcurrentHashMap<>();

    // 保存所有连接的会话，key: pictureId, value: 用户会话集合
    private final Map<Long, Set<WebSocketSession>> pictureSessions = new ConcurrentHashMap<>();


    // 提供getter方法供策略类访问
    // 不让直接访问属性字段 避免破坏封装性 现在就是部分有限访问和修改
    public Map<Long, Long> getPictureEditingUsers() {
        return pictureEditingUsers;
    }

    /**
     * 建立连接
     * @param session
     * @throws Exception
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 保存会话到集合中
        User user = (User) session.getAttributes().get("user");
        Long pictureId = (Long) session.getAttributes().get("pictureId");
        /**
         * 检查pictureSessions中是否已存在pictureId对应的键值对。
         * 如果不存在，则为该pictureId创建一个新的并发安全的Set集合，并将其放入pictureSessions中。
         *
         * ConcurrentHashMap.newKeySet() 是 Java 提供的一种线程安全的集合工具方法，用于创建一个线程安全的 Set 集合。
         */
        pictureSessions.putIfAbsent(pictureId, ConcurrentHashMap.newKeySet());
        pictureSessions.get(pictureId).add(session);

        // 构造响应
        PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
        pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.INFO.getValue());
        String message = String.format("%s加入编辑", user.getUserName());
        pictureEditResponseMessage.setMessage(message);
        pictureEditResponseMessage.setUser(userService.getUserVO(user));
        // 广播给同一张图片的用户
        //因为只是单纯的消息通知 ，不会造成像编辑的多次操作的影响 所以可以给自己也发广播
        //效果就是当前用户也可以在页面里看到自己  xxx加入了编辑
        broadcastToPicture(pictureId, pictureEditResponseMessage);
    }


    /**
     * 处理文本消息
     * @param session
     * @param message
     * @throws Exception
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 将消息解析为 PictureEditMessage
        PictureEditRequestMessage pictureEditRequestMessage = JSONUtil.toBean(message.getPayload(), PictureEditRequestMessage.class);
        String type = pictureEditRequestMessage.getType();
        // 根据 type 值找到对应的枚举项
        PictureEditMessageTypeEnum pictureEditMessageTypeEnum = PictureEditMessageTypeEnum.valueOf(type);


        // 从 Session 属性中获取公共参数
        Map<String, Object> attributes = session.getAttributes();
        User user = (User) attributes.get("user");
        Long pictureId = (Long) attributes.get("pictureId");

//        // 调用对应的消息处理方法
//        //对应方法会给前端进行响应 目的是为了通知其他用户 当前进行操作的用户正在干什么操作
        /**
         * 这里不要迷惑 为什么switch里不是用pictureEditMessageTypeEnum.getValue()
         * 因为这里的case条件它不是字符串呀  它本身也是枚举呢 case ENTER_EDIT
         * 所以直接用pictureEditMessageTypeEnum
         */
//        switch (pictureEditMessageTypeEnum) {
//            case ENTER_EDIT:
//                handleEnterEditMessage(pictureEditRequestMessage, session, user, pictureId);
//                break;
//            case EDIT_ACTION:
//                handleEditActionMessage(pictureEditRequestMessage, session, user, pictureId);
//                break;
//            case EXIT_EDIT:
//                handleExitEditMessage(pictureEditRequestMessage, session, user, pictureId);
//                break;
//            default:
//                PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
//                pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.ERROR.getValue());
//                pictureEditResponseMessage.setMessage("消息类型错误");
//                pictureEditResponseMessage.setUser(userService.getUserVO(user));
//                session.sendMessage(new TextMessage(JSONUtil.toJsonStr(pictureEditResponseMessage)));
//        }
        // 上面的switch 太臃肿 即三种策略都在一个类里面编写 并且需要判断应该使用哪个策略 同时不利于后续的扩展
        // 根据消息类型处理消息（生产者的消息写入到disruptor队列里）
        pictureEditEventProducer.publishEvent(pictureEditRequestMessage, session, user, pictureId);

    }


    /**
     * 进入编辑状态
     * @param pictureEditRequestMessage
     * @param session
     * @param user
     * @param pictureId
     */
    public void handleEnterEditMessage(PictureEditRequestMessage pictureEditRequestMessage, WebSocketSession session, User user, Long pictureId) throws Exception {
//        // 没有其他用户在编辑图片，才可以进行编辑
//        if (!pictureEditingUsers.containsKey(pictureId)) {
//            pictureEditingUsers.put(pictureId, user.getId());
//            // 构造响应
//            PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
//            pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.ENTER_EDIT.getValue());
//            String message = String.format("用户%s开始编辑图片", user.getUserName());
//            pictureEditResponseMessage.setMessage(message);
//            pictureEditResponseMessage.setUser(userService.getUserVO(user));
//            // 广播给同一张图片的用户
//            //因为只是单纯的消息通知 ，不会造成像编辑的多次操作的影响 所以可以给自己也发广播
//            //效果就是当前用户也可以在页面里看到自己  xxx开始了编辑
//            broadcastToPicture(pictureId, pictureEditResponseMessage);
//        }

        // 原子操作：如果key不存在则put，返回null；否则返回已存在的value
        // putIfAbsent 是原子操作，确保检查和插入的整个过程不会被其他线程打断。
        Long previousEditor = pictureEditingUsers.putIfAbsent(pictureId, user.getId());
        // 如果previousEditor为null，则表示成功获取编辑权限(前面没有其他用户在占用)，否则表示图片已被其他用户编辑
        if (previousEditor == null) {
            // 成功获取编辑权限
            PictureEditResponseMessage response = new PictureEditResponseMessage();
            response.setType(PictureEditMessageTypeEnum.ENTER_EDIT.getValue());
            response.setMessage(String.format("%s开始编辑图片", user.getUserName()));
//            pictureEditResponseMessage.setUser(userService.getUserVO(user));
//            // 广播给同一张图片的用户
//            //因为只是单纯的消息通知 ，不会造成像编辑的多次操作的影响 所以可以给自己也发广播
//            //效果就是当前用户也可以在页面里看到自己  xxx开始了编辑
            broadcastToPicture(pictureId, response);
        } else {
            // 图片已被其他用户编辑
            /**
             * 直接向当前 WebSocket 连接对应的客户端发送消息，但不会自动广播给其他客户端。这是 WebSocket 的单点消息发送机制
             * 如果想要广播 那就是像broadcastToPicture里面一样 把每个session会话取出来 进行发送消息
             */
            session.sendMessage(new TextMessage("当前图片正在被其他用户编辑"));
        }

    }

    /**
     * 执行编辑动作的消息
     * @param pictureEditRequestMessage
     * @param session
     * @param user
     * @param pictureId
     */
    public void handleEditActionMessage(PictureEditRequestMessage pictureEditRequestMessage, WebSocketSession session, User user, Long pictureId) throws Exception {
//        校验当前登录用户是否是当前正在编辑图片的用户
        Long editUserId = pictureEditingUsers.get(pictureId);
//        编辑对应的动作是否存在  否就拒绝
        String editAction = pictureEditRequestMessage.getEditAction();
        PictureEditActionEnum enumByValue = PictureEditActionEnum.getEnumByValue(editAction);
        if (enumByValue == null) {
            return;
        }
        if (editUserId != null && editUserId.equals(user.getId())) {
//            通知其他用户 我在干什么操作
            // 构造响应
            PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
            pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.ENTER_EDIT.getValue());
            String message = String.format("%s执行%s", user.getUserName(), enumByValue.getText());
            pictureEditResponseMessage.setMessage(message);
            pictureEditResponseMessage.setUser(userService.getUserVO(user));
            pictureEditResponseMessage.setEditAction(editAction);
            // 广播给同一张图片的用户
            // 这里就不能给自己发了 因为涉及到了 编辑的具体某个动作指令 后续会执行 避免重复执行
            broadcastToPicture(pictureId, pictureEditResponseMessage, session);
        }
    }

    /**
     * 退出编辑的消息通知  这种 像突然关闭整个网页 就会执行
     * @param pictureEditRequestMessage
     * @param session
     * @param user
     * @param pictureId
     */
    public void handleExitEditMessage(PictureEditRequestMessage pictureEditRequestMessage, WebSocketSession session, User user, Long pictureId) throws Exception {
        Long editingUserId = pictureEditingUsers.get(pictureId);
        if (editingUserId != null && editingUserId.equals(user.getId())) {
            // 移除当前用户的编辑状态
            pictureEditingUsers.remove(pictureId);
            // 构造响应，发送退出编辑的消息通知
            PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
            pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.EXIT_EDIT.getValue());
            String message = String.format("%s退出编辑图片", user.getUserName());
            pictureEditResponseMessage.setMessage(message);
            pictureEditResponseMessage.setUser(userService.getUserVO(user));
            broadcastToPicture(pictureId, pictureEditResponseMessage);
        }
    }


    /**
     * 关闭连接
     * 这个只有在用户比如点击关闭网页的时候才会执行  正常点击退出编辑 是不会触发该方法的
     * @param session
     * @param status
     * @throws Exception
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, @NotNull CloseStatus status) throws Exception {
        Map<String, Object> attributes = session.getAttributes();
        Long pictureId = (Long) attributes.get("pictureId");
        User user = (User) attributes.get("user");
        // 移除当前用户的编辑状态
        handleExitEditMessage(null, session, user, pictureId);

        // 删除会话
        Set<WebSocketSession> sessionSet = pictureSessions.get(pictureId);
        if (sessionSet != null) {
            sessionSet.remove(session);
            // 这里是因为 可能还有其他用户在进行会话连接呢，所以你只能把自己当前的会话进行删除
            // 除非别人都不在了 即会话列表为空了 那么就可以删除对应的key了
            if (sessionSet.isEmpty()) {
                pictureSessions.remove(pictureId);
            }
        }

        // 响应
        PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
        pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.INFO.getValue());
        String message = String.format("用户%s离开编辑", user.getUserName());
        pictureEditResponseMessage.setMessage(message);
        pictureEditResponseMessage.setUser(userService.getUserVO(user));
        broadcastToPicture(pictureId, pictureEditResponseMessage);
    }


    // 全部广播
    private void broadcastToPicture(Long pictureId, PictureEditResponseMessage pictureEditResponseMessage) throws Exception {
        broadcastToPicture(pictureId, pictureEditResponseMessage, null);
    }

    public void broadcastToPicture(Long pictureId, PictureEditResponseMessage pictureEditResponseMessage, WebSocketSession excludeSession) throws Exception {
        Set<WebSocketSession> sessionSet = pictureSessions.get(pictureId);
        if (CollUtil.isNotEmpty(sessionSet)) {
            // 这样直接转换后 会有问题，就是 Long 类型会丢失精度(就是前端js接受不了那么大的值)，所以需要配置一下
            //之前http请求哪里是统一编写了配置类解决的，这里需要手动解决一下
//            String message = JSONUtil.toJsonStr(pictureEditResponseMessage);
            // 创建 ObjectMapper
            ObjectMapper objectMapper = new ObjectMapper();
            // 配置序列化：将 Long 类型转为 String，解决丢失精度问题
            SimpleModule module = new SimpleModule();
            module.addSerializer(Long.class, ToStringSerializer.instance);
            module.addSerializer(Long.TYPE, ToStringSerializer.instance); // 支持 long 基本类型
            // 注册自定义序列化器模块
            objectMapper.registerModule(module);

            // 序列化为 JSON 字符串
            String message = objectMapper.writeValueAsString(pictureEditResponseMessage);
            TextMessage textMessage = new TextMessage(message);
            for (WebSocketSession session : sessionSet) {
                // 排除掉的 session 不发送  这个是不给当前生产者发送对应事件（也就是谁会自己给自己还再发送一遍指令呢？二次执行了）
                if (excludeSession != null && excludeSession.equals(session)) {
                    continue;
                }
                if (session.isOpen()) {
                    session.sendMessage(textMessage);
                }
            }
        }
    }

}

